Coverage Report

Created: 2026-02-05 09:02

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\scloud-dns\scloud-dns\src\dns\zones\zone_parser.rs
Line
Count
Source
1
use crate::dns::q_class::DNSClass;
2
use crate::dns::q_type::DNSRecordType;
3
use crate::dns::records::DNSRecord;
4
use crate::dns::zones::Zone;
5
use crate::exceptions::SCloudException;
6
use std::collections::HashMap;
7
use std::fs::File;
8
use std::io::{self, BufRead};
9
10
/// Parse a DNS zone file and build an in-memory `Zone` structure.
11
///
12
/// The zone file must be located in the `zones/` directory and named
13
/// `<qname>.zone`.
14
///
15
/// This parser supports:
16
/// - `$TTL` directive (default TTL)
17
/// - `$ORIGIN` directive
18
/// - SOA record (unique per zone)
19
/// - Common DNS record types: A, AAAA, NS, MX, TXT, SOA, CNAME, PTR, SRV, CAA, NAPTR
20
///
21
/// Records are stored by owner name in a `HashMap<String, Vec<DNSRecord>>`.
22
/// The SOA record is stored separately in `zone.soa`.
23
///
24
/// # Arguments
25
/// * `qname` - The zone name (used to locate the zone file)
26
///
27
/// # Errors
28
/// Returns `SCloudException` if:
29
/// - the zone file cannot be found
30
/// - the file is empty or unreadable
31
/// - TTL parsing fails
32
///
33
/// # Example
34
/// ```
35
/// use crate::dns::zones::zone_parser;
36
///
37
/// let zone = zone_parser("example.com").expect("Failed to parse zone");
38
///
39
/// assert!(zone.soa.is_some());
40
/// assert!(!zone.records.is_empty());
41
/// ```
42
#[allow(unused)]
43
1
pub fn zone_parser(qname: &str) -> Result<Zone, SCloudException> {
44
1
    let filename = format!("zones/{}.zone", qname);
45
1
    let file =
46
1
        File::open(&filename).map_err(|_| SCloudException::SCLOUD_ZONE_PARSER_FILE_NOT_FOUND)
?0
;
47
48
1
    let mut zone = Zone {
49
1
        origin: None,
50
1
        name: String::new(),
51
1
        ttl: 3600,
52
1
        soa: None,
53
1
        records: HashMap::new(),
54
1
    };
55
56
1
    let mut default_ttl = 3600u32;
57
58
90
    for line in 
io::BufReader::new1
(
file1
).
lines1
() {
59
90
        let line = line.map_err(|_| SCloudException::SCLOUD_ZONE_PARSER_FILE_EMPTY)
?0
;
60
90
        let line = line.trim();
61
62
90
        if line.is_empty() || 
line73
.
starts_with73
(';') {
63
51
            continue;
64
39
        }
65
66
39
        let line = if let Some(
idx6
) = line.find(';') {
67
6
            &line[..idx]
68
        } else {
69
33
            line
70
        }
71
39
        .trim();
72
73
39
        if line.starts_with("$TTL") {
74
1
            if let Some(ttl_str) = line.split_whitespace().nth(1) {
75
1
                default_ttl = ttl_str
76
1
                    .parse::<u32>()
77
1
                    .map_err(|_| SCloudException::SCLOUD_ZONE_PARSER_FAILED_TO_READ_TTL_FIELD)
?0
;
78
1
                zone.ttl = default_ttl;
79
0
            }
80
1
            continue;
81
38
        }
82
83
38
        if line.starts_with("$ORIGIN") {
84
1
            if let Some(origin_str) = line.split_whitespace().nth(1) {
85
1
                zone.origin = Some(origin_str.to_string());
86
1
            
}0
87
1
            continue;
88
37
        }
89
90
37
        let mut parts = line.split_whitespace();
91
37
        let name = match parts.next() {
92
37
            Some(n) => n.to_string(),
93
0
            None => continue,
94
        };
95
96
37
        let 
next31
= match parts.next() {
97
31
            Some(n) => n,
98
6
            None => continue,
99
        };
100
101
31
        let (ttl, class, type_str) = if let Ok(
parsed_ttl1
) = next.parse::<u32>() {
102
1
            let class = parts.next().unwrap_or("IN");
103
1
            let type_str = parts.next().unwrap_or_default();
104
1
            (parsed_ttl, class.to_string(), type_str)
105
30
        } else if next.eq_ignore_ascii_case("IN") || 
next0
.
eq_ignore_ascii_case0
(
"CH"0
) {
106
30
            let class = next;
107
30
            let type_str = parts.next().unwrap_or_default();
108
30
            (default_ttl, class.to_string(), type_str)
109
        } else {
110
0
            (default_ttl, "IN".to_string(), next)
111
        };
112
113
31
        let rclass = match class.to_uppercase().as_str() {
114
31
            "IN" => DNSClass::IN,
115
0
            "CS" => DNSClass::CS,
116
0
            "CH" => DNSClass::CH,
117
0
            "HS" => DNSClass::HS,
118
0
            "NONE" => DNSClass::NONE,
119
0
            "ANY" => DNSClass::ANY,
120
0
            _ => continue,
121
        };
122
123
31
        let rtype = match type_str.to_uppercase().as_str() {
124
31
            "A" => 
DNSRecordType::A10
,
125
21
            "AAAA" => 
DNSRecordType::AAAA4
,
126
17
            "NS" => 
DNSRecordType::NS3
,
127
14
            "MX" => 
DNSRecordType::MX2
,
128
12
            "TXT" => 
DNSRecordType::TXT3
,
129
9
            "SOA" => 
DNSRecordType::SOA1
,
130
8
            "CNAME" => 
DNSRecordType::CNAME2
,
131
6
            "PTR" => 
DNSRecordType::PTR1
,
132
5
            "SRV" => 
DNSRecordType::SRV2
,
133
3
            "CAA" => 
DNSRecordType::CAA2
,
134
1
            "NAPTR" => DNSRecordType::NAPTR,
135
0
            _ => continue,
136
        };
137
138
31
        let value_parts: Vec<&str> = parts.collect();
139
140
31
        let value_str = if rtype == DNSRecordType::TXT {
141
3
            value_parts.join(" ")
142
        } else {
143
28
            value_parts.join(" ")
144
        };
145
146
31
        let mut record = DNSRecord {
147
31
            name: name.clone(),
148
31
            rtype: rtype.clone(),
149
31
            rclass: rclass.clone(),
150
31
            ttl,
151
31
            value: value_str,
152
31
            priority: None,
153
31
            weight: None,
154
31
            port: None,
155
31
            flags: None,
156
31
            tag: None,
157
31
            regex: None,
158
31
            replacement: None,
159
31
            order: None,
160
31
            preference: None,
161
31
        };
162
163
31
        match rtype {
164
            DNSRecordType::MX => {
165
2
                if value_parts.len() >= 2 {
166
2
                    if let Ok(prio) = value_parts[0].parse::<u16>() {
167
2
                        record.priority = Some(prio);
168
2
                        record.value = value_parts[1..].join(" ");
169
2
                    
}0
170
0
                }
171
            }
172
            DNSRecordType::SRV => {
173
2
                if value_parts.len() >= 4 {
174
2
                    record.priority = value_parts[0].parse().ok();
175
2
                    record.weight = value_parts[1].parse().ok();
176
2
                    record.port = value_parts[2].parse().ok();
177
2
                    record.value = value_parts[3..].join(" ");
178
2
                
}0
179
            }
180
            DNSRecordType::CAA => {
181
2
                if value_parts.len() >= 3 {
182
2
                    record.flags = value_parts[0].parse().ok();
183
2
                    record.tag = Some(value_parts[1].to_string());
184
2
                    record.value = value_parts[2..].join(" ");
185
2
                
}0
186
            }
187
            DNSRecordType::NAPTR => {
188
1
                if value_parts.len() >= 5 {
189
1
                    record.order = value_parts[0].parse().ok();
190
1
                    record.preference = value_parts[1].parse().ok();
191
1
                    record.flags = Some(value_parts[2].chars().next().unwrap_or_default() as u8);
192
1
                    record.regex = Some(value_parts[3].to_string());
193
1
                    record.replacement = Some(value_parts[4].to_string());
194
1
                
}0
195
            }
196
24
            _ => {}
197
        }
198
199
31
        match rtype {
200
1
            DNSRecordType::SOA => zone.soa = Some(record),
201
30
            _ => zone.records.entry(name).or_default().push(record),
202
        }
203
    }
204
205
1
    Ok(zone)
206
1
}